package model;

import java.io.IOException;
import java.util.List;

import android.app.Activity;
import android.graphics.Rect;
import android.hardware.Camera;
import android.util.Log;
import android.view.Surface;

import com.google.zxing.PlanarYUVLuminanceSource;

/**
 * Camera manager
 */
public class CameraManager 
{
	//Singleton
	//Volatile means java will always get this variable from main memory
	private static volatile CameraManager INSTANCE;
	
	private CameraManager()
	{
		setCameraInstance();
	}

	
	public static CameraManager getInstance()
	{
		if(INSTANCE == null)
		{
			//synchronized means only one thread can use the variable at a time (locks the class)
			synchronized (CameraManager.class) 
			{
				if(INSTANCE == null)
				{
					INSTANCE = new CameraManager();
				}
			}
		}
		return INSTANCE;
	}
	
	
	//Fraction of bounds size and height in view.
	//Set to 1 to use full screen/
    private static final double BOUNDS_FRACTION = 1;

    private Camera camera;
    private int cameraId;
    
    /**
     * Current orientation of camera
     * Possible values : 0, 90, 180, 270
     */
    private int orientation;


    
    public void reconnectCamera()
    {
    	try 
    	{
    		if(camera == null)
    		{
    			setCameraInstance();
    		}
    		else
    		{
    			camera.reconnect();
    		}
    		
		} catch (IOException e) {
			e.printStackTrace();
		}
    }
    
    /**
     * Getter for camera
     *
     * @return camera instance, if it has been initialized
     */
    public Camera getCamera() 
    {
        return camera;
    }

    /**
     * Starts preview of camera, if it has been initialized
     */
    public synchronized void startPreview() 
    {
        if (camera != null)
        {
            camera.startPreview();
            Log.d("CAMERA", "startPreview() - Camera says yes");
        }
        else 
    	{
    		Log.e("CAMERA", "startPreview() - Camera says no");
    	}
    }

    /**
     * Stops preview of camera, if it has been initialized
     */
    public synchronized void stopPreview() 
    {
        if (camera != null) 
        {
            camera.stopPreview();
        }
    }

    /**
     * Release camera, if it has been initialized
     */
    public synchronized void release() 
    {
        if (camera != null) 
        {
        	Log.v("CAMERA", "release!");
            camera.release();
            camera = null;
        }
    }

    /**
     * @return if camera has been initialized<br/>( <code>camera != null</code> )
     */
    public synchronized boolean hasCamera() 
    {
        return camera != null;
    }

    /**
     * @return bounding rect for ui
     */
    public final synchronized Rect getBoundingRectUi(int uiWidth, int uiHeight) 
    {
        return createRectangle(uiWidth, uiHeight);
    }

    /**
     * @return bounding rect for camera
     */
    public final synchronized Rect getBoundingRect() 
    {
        if (camera != null) {
            Camera.Size previewSize = camera.getParameters().getPreviewSize();
            return createRectangle(previewSize.width, previewSize.height);
        }
        return null;
    }

    //calculates the required numbers and creates a rectangle with them, then returns that rectangle
    private Rect createRectangle(int calcWidth, int calcHeight)
    {
    	int height = (int) (calcHeight * BOUNDS_FRACTION);
        int width = (int) (calcWidth * BOUNDS_FRACTION);
        
        int left = (int) (calcWidth * ((1 - BOUNDS_FRACTION) / 2));
        int top = (int) (calcHeight * ((1 - BOUNDS_FRACTION) / 2));
        
        int right = left + width;
        int bottom = top + height;    	
    	
        return new Rect(left, top, right, bottom);
    }
    
    /**
     * executes <br/> <code>camera.setOneShotPreviewCallback(callback)</code> if <br/>
     * <code>camera != null</code>
     * @param callback callback to provide
     */
    public synchronized void requestNextFrame(Camera.PreviewCallback callback) {
        if (camera != null) 
        {
        	Log.d("CAMERA", "RequestNextFrame() - Camera says yes");
        	
        	camera.setPreviewCallback(callback);
        }
        else
        {
        	Log.e("CAMERA", "RequestNextFrame() - Camera says no");
        }
    }

    /**
     * A factory method to build the appropriate LuminanceSource object based on the format
     * of the preview buffers, as described by Camera.Parameters.
     *
     * @param data   A preview frame.
     * @param width  The width of the image.
     * @param height The height of the image.
     * @return A PlanarYUVLuminanceSource instance.
     */
    public synchronized PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height, Rect boundingRect) 
    {
        switch (orientation) 
        {
            case 0:
                break;
            case 90:
            	rotate90(data, width, height);
                return new PlanarYUVLuminanceSource(data, height, width, boundingRect.top, boundingRect.left,
                        boundingRect.height(), boundingRect.width(), false);
            case 180:
                break;
            case 270:
            	rotate90(data, width, height);
            	break;
        }

        return new PlanarYUVLuminanceSource(data, width, height, boundingRect.left, boundingRect.top,
                boundingRect.width(), boundingRect.height(), false);
    }


    /**
     * Sets camera display orientation depending on current activity orientation
     * @param activity activity, which holds camera preview
     */
    public void setCameraDisplayOrientation(Activity activity) {
    	if(camera == null)
    		return;
    	
    	Log.v("CAMERA", "SetCameraDisplayOrientation() - Camera != null");
    	
        android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(cameraId, info);
        
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        
        int result = calculateDisplayOrientationDegrees(info, rotation);
        
    	camera.setDisplayOrientation(result);
    }
    
    private int calculateDisplayOrientationDegrees(android.hardware.Camera.CameraInfo info, int rotation)
    {
    	int degrees = 0;
        
        switch (rotation) {
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }

        int result;
        
        boolean currentCameraIsFrontFacingCamera = info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT;
        
        if (currentCameraIsFrontFacingCamera) {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360; // compensate the mirror
        } 
        else 
        { // back-facing
            result = (info.orientation - degrees + 360) % 360;
        }        
        
        orientation = result;
        
        return result;
    }


    /**
     * A safe way to get an instance of the Camera object.
     */
    private void setCameraInstance() {
    	Log.v("CAMERA", "setCameraInstance() - open camera");
        Camera c = null;
        try {
            cameraId = 0;
            c = Camera.open(); // attempt to get a Camera instance
            Camera.Parameters p = c.getParameters();
            p.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);

            //Set the lowest capture rate possible with the camera on the system.
            List<int[]> fpsrange = p.getSupportedPreviewFpsRange();            
            p.setPreviewFpsRange(fpsrange.get(fpsrange.size()-1)[0], fpsrange.get(fpsrange.size()-1)[1] );
            
            //Set the highest possible resolution
            List<Camera.Size> vidRanges = p.getSupportedPreviewSizes();
            p.setPreviewSize(vidRanges.get(0).width, vidRanges.get(0).height);
            
            Log.v("CAMERA", String.format("{%d x %d}", vidRanges.get(0).width, vidRanges.get(0).height));
            
//            MediaRecorder mr = new MediaRecorder();
//            mr.setVideoSource(MediaRecorder.VideoSource.CAMERA);
//            mr.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
//            mr.prepare();
//            mr.start();
            
            //Apply changes
            c.setParameters(p);
            
            
        } 
        catch (Exception e) 
        {
            // Camera is not available (in use or does not exist)
            Log.e(CameraManager.class.getSimpleName(), "Camera error", e);
        }
        camera = c;
    }
    
    /**
     * Rotates image data
     * @param data raw image data
     * @param width width of image
     * @param height height of image
     */
    public void rotate90(byte[] data, int width, int height) {
        int length = height * width;
        int lengthDecimal = length - 1;
        int i = 0;
        do 
        {
        	// k = the spot in byte[] to swap with i if it is not equal to i after calculations.
	        int k = (i * height) % lengthDecimal;
	        
	        while (k > i) 
	    	{
	        	k = (height * k) % lengthDecimal;
	    	}
	        
	        if (k != i)
	    	{
	    		swap(data, k, i);
	    	}
	        
        } while (++i <= (length - 2));
    }
    
    /**
     * Swaps two elements in array
     * @param data array
     * @param k first element to swap
     * @param i second element to swap
     */
    private static void swap(byte[] data, int k, int i) {
        byte temp = data[k];
        data[k] = data[i];
        data[i] = temp;
    }

}


